17.1 Class基本语法
17.1.1 概述
说明: ES6
提供了更接近传统语言的写法,引入了 Class
(类)这个概念,作为对象的模板。通过 class
关键字,可以定义类。
ES6
的类,完全可以看作构造函数的另一种写法(类的数据类型就是函数,类本身就指向构造函数)- 使用时,对类使用
new
命令,跟构造函数的用法完全一致 - 构造函数的
prototype
属性,在ES6
的“类”上面继续存在,类的所有方法都定义在类的prototype
属性上面 prototype
对象的constructor
属性,直接指向“类”的本身,这与ES5
的行为是一致的- 类的属性名,可以采用表达式(和对象字面量相同)
- 方法之间不需要逗号分隔,加了会报错(和对象字面量不同)
和构造函数比较: ES6
的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5
的构造函数都可以做到,新的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。不同点是
- 类的内部所有定义的方法,都是不可枚举的
- 类不能作为普通方法调用
Class
声明不会被提升
Demo: Point 类
ES5 写法
1 | function Point(x, y) { |
ES6写法
1 | let methodName = "getArea"; |
17.1.2 constructor 方法
说明:通过 new
命令生成对象实例时,自动调用该方法
- 类必须有
constructor
方法,如果没有显式定义,一个空的constructor
方法(constructor() {}
)会被默认添加 constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象
注意:不使用 new
是没法调用的,会报错
1 | class Foo { |
17.1.3 创建实例
说明:与 ES5
完全一样,也是使用 new
命令
注意:如果忘记加上 new
,像函数那样调用 Class
,将会报错
技巧:可以通过实例的 __proto__
属性为 Class
添加方法(不推荐使用,因为这会改变 class
的原始定义,影响到所有实例)
1 | //定义类 |
17.1.4 不存在变量提升
说明: ES6
不会把类的声明提升到代码头部
目的:当使用继承特性时,保证子类在父类之后定义
1 | new Foo(); // ReferenceError |
17.1.5 Class 表达式
说明:与函数一样,类也可以使用表达式的形式定义
注意:class
关键字后面的类名只在类定义部分使用,可以不给出,类似函数表达式
技巧:采用 Class
表达式,可以写出立即执行(实例化)的 Class
Demo1: 不省略 class 后面的类名
1 | const MyClass = class Me { |
Demo2: 省略 class 后面的类名
1 | const MyClass = class { /* ... */ }; |
Demo3: 立即实例化的 class
1 | let person = new class { |
17.1.6 私有方法
说明:私有方法是常见需求,但 ES6
不提供,只能通过变通方法模拟实现,比如以下3中方式
方式一: 为私有方法名添加_
前缀
说明:告知这是一个只限于内部使用的私有方法
注意:只要想调用,在类的外部,还是可以调用到这个方法
1 | class Widget { |
方式二:私有方法定义在类之外定义
说明:模块(比如ES6
模块)内部的方法只要不 export
出去,只在类中需要使用该方法的地方调用它,从而达到了私有方法的效果。
1 | class Widget { |
方式三:命名为一个 Symbol
值
说明:利用 Symbol
值的唯一性,将私有方法的名字命名为一个 Symbol
值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果
1 | const bar = Symbol('bar'); |
17.1.7 this的指向
说明:类的方法内部如果含有 this
,它默认指向类的实例。
注意: this
的指向是可以改变的,因此如果有方法使用了 this
,则要小心调用以确保 this
的指向符合预期。
Demo:this 指向被不符合预期导致错误
1 | class Logger { |
确保 this 的指向
方法1: 在构造方法中绑定 this
1 | class Logger { |
方法2: 使用箭头函数(推荐)
1 | class Logger { |
方法3: 使用 Proxy,获取方法的时候,自动绑定 this
1 | class Logger { |
17.1.8 严格模式
说明:类和模块(Es6 模块
)的内部,默认就是严格模式,所以不需要使用 use strict
指定运行模式。
17.1.9 name 属性
说明:本质上,ES6
的类只是 ES5
的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括 name
属性
值:紧跟在 class
关键字后面的类名
1 | class Point {} |
17.2 Class的继承
17.2.1 基本用法
extends
说明:Class 之间可以通过 extends
关键字实现继承
价值:这比 ES5
的通过修改原型链实现继承,要清晰和方便很多
对比 | 说明 |
---|---|
ES5 的继承 |
先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面(Parent.apply(this) ) |
ES6 的继承 |
先创造父类的实例对象 this (所以必须先调用super方法),然后再用子类的构造函数修改 this |
constructor
说明:如果子类没有定义 constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor
方法。
1 | constructor(...args) { |
super
说明:在子类的构造函数中,只有调用 super
之后,才可以使用 this
关键字,否则会报错。因为子类实例的构建,是基于对父类实例加工,只有 super
方法才能返回父类实例。
注意: super
是一个特殊的关键字,代表父类的实例
- 当作函数调用时,会执行父类的构造器完成父类实例的创建
- 当作为对象使用时,作为对父类实例的引用
1 | class Point { |
17.2.2 类的prototype属性和proto属性
说明: Class
作为构造函数的语法糖,继承是依靠原型链实现的来实现的,ES6
的父子类之间存在两条原型链
- 作为一个对象,子类的原型(proto属性)是父类
- 作为一个构造函数,子类的原型(prototype属性)是父类的实例
1 | // 父类 |
17.2.3 Extends 的继承目标
说明:有 3 种特殊情况
(1)子类继承 Object
类
说明:和普通情况类似,只不过父类换成了 Object
,平淡无奇
特性展示
1 | class A extends Object { |
(2)不存在任何继承
说明:这种类的实例基本可以看作 Object
的实例(或者类比对象字面量)
特性展示
1 | class A { |
(3)子类继承 null
说明:这种类的实例不继承任何属性和方法
特性展示
1 | class A extends null { |
等价形式
1 | class C extends null { |
17.2.4 Object.getPrototypeOf()
说明: 等价于 目标.__proto__
技巧:判断,一个类是否继承了另一个类
Demo: 判断 ColoePoint 是不是 Point 的子类
1 | Object.getPrototypeOf(ColorPoint) === Point |
17.2.5 super 关键字
说明: 有两种用法
- 作为函数调用时(即
super(...args)
),super
代表父类的构造函数。 - 作为对象调用时(即
super.prop
或super.method()
),super
代表父类。注意,此时super
即可以引用父类实例的属性和方法,也可以引用父类的静态方法。
注意: 不同于 Java
等,JS
的类的静态成员不能通过实例访问到,只能通过类名来访问,使用 super
关键字却可以在子类定义中访问到父类的静态方法。
技巧:对象(指对象字面量)总是继承其他对象的,所以可以在任意一个对象中,使用 super
关键字
Demo1: 作为对象调用时
1 | class B extends A { |
Demo2: 在对象字面量中使用
1 | var obj = { |
17.2.6 实例的 __proto__
属性
说明:子类实例的 __proto__
属性的 __proto__
属性,指向父类实例的 __proto__
属性。也就是说,子类的原型的原型,是父类的原型。
技巧:通过子类实例的 __proto__.__proto__
属性,可以修改父类实例的行为。注意,这会影响所有父类和子类的实例。
Demo: ColorPoint 是 Point 的子类
1 | var p1 = new Point(2, 3); |
17.3 原生构造函数的继承
17.3.1 原生构造函数
说明:指语言内置的构造函数,通常用来生成数据结构。
ECMAScript 的原生构造函数(9个) |
---|
Boolean() |
Number() |
String() |
Array() |
Date() |
Function() |
RegExp() |
Error() |
Object() |
17.3.2 错误的继承方式
虽然很想只根据原书做笔记,但个人认为说使用 ES5 无法实现对原生构造函数的继承有些片面。 ES5 实现继承有很多方式,只有某些方式有问题而已,而有问题的地方特指试图通过 call 或 apply 调用父类构造期构建实例属性这种行为。
说明: ES5
的实现继承的方式很多,有问题的是以下的方式。核心就两点,问题出在第一点
- 继承实例成员:通过
apply
或call
调用父类的构造函数 - 继承静态成员:重写子类
prototype
,使子类实例能够通过原型链最终访问到父类原型上的成员
原因:当通过 apply
或 call
调用原生构造函数时,传入的参数(用来绑定 this
的对象,也就是子类实例)会被忽略。导致无法继承父类的实例成员。
Demo: 演示用这种方式实现对 Array 的继承
1 | /** |
17.3.3 正确的继承方式
说明: ES6
的 extends
关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构
原理: ES6
是先新建父类的实例对象 this
,然后再用子类的构造函数修饰 this
,使得父类的所有行为都可以继承
技巧:其实用 ES5
模拟 ES6
实现继承的原理一样可以实现对原生构造函数的继承。
注意: ES6
改变了 Object
构造函数的行为,一旦发现 Object
方法不是通过 new Object()
这种形式调用,会忽略参数。
Demo1: 继承 Array
1 | class MyArray extends Array { |
Demo2: 带版本管理功能的 Array
1 | class VersionedArray extends Array { |
Demo3: 继承 Error
1 | class ExtendableError extends Error { |
Demo4: 继承 Object
1 | class NewObj extends Object{ |
17.4 Class的取值函数(getter)和存值函数(setter)
说明: 可以看做 ES5
的属性的描述对象上的 setter
和 getter
的语法糖
- 在
Class
内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为 setter
和getter
是设置在属性的descriptor
对象上的
Demo1: 基本使用
1 | class MyClass { |
Demo2: setter 和 getter 是设置在属性的 descriptor 对象上的
1 | class CustomHTMLElement { |
17.5 Class的Generator方法
说明:和普通 Generator
函数一样,如果某个方法之前加上 *
,就表示该方法是一个 Generator
函数
- 类的
Symbol.iterator
方法返回一个类的默认遍历器 for...of
循环等会自动调用对象的Symbol.iterator
这个遍历器
1 | class Foo { |
17.6 Class的静态方法
关键字: static
说明:如果在一个方法前,加上 static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 静态方法
。
- 如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法
- 父类的静态方法,可以被子类继承
- 父类的静态方法也可以在子类中从
super
对象上调用的
1 | class Foo { |
17.7 Class的静态属性和实例属性
17.7.1 Class 的静态属性(ES6)
静态属性:静态属性指的是 Class
本身的属性,即 ClassName.propname
,而不是定义在实例对象( this
)上的属性
注意: static
只能用来定义静态方法,不能定义静态属性
正确用法
1 | class Foo { |
错误用法
1 | // 以下两种写法都无效 |
17.7.2 ES7 相关提案
说明:ES7
有一个提案,对实例属性和静态属性,都规定了新的写法。目前 Babel
转码器支持
类的实例属性
说明:类的实例属性可以用等式,写入类的定义之中
注意:以前,定义实例属性,只能写在类的 constructor
方法里面。
技巧:对于那些在 constructor
里面已经定义的实例属性,新写法允许直接列出
Demo1: 基本使用
1 | class MyClass { |
Demo2: 在 constructor 里面已经定义的实例属性,新写法允许直接列出
1 | class ReactCounter extends React.Component { |
类的静态属性
说明:类的静态属性只要在上面的实例属性写法前面,加上 static
关键字就可以了
注意:新写法是显式声明(declarative),而不是赋值处理,语义更好。
老写法
1 | class Foo { |
新写法
1 | class Foo { |
17.8 new.target属性
说明: ES6为 new
关键字引入了一个 new.target
属性,(在构造函数中)返回 new
命令作用于的那个构造函数。
- 需要注意的是,子类继承父类时,
new.target
会返回子类 Class
内部调用new.target
,返回当前Class
用途:如果构造函数不是通过 new
命令调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
注意:在函数外部,使用 new.target
会报错。
Demo1: 确保构造函数只能通过 new 命令调用
方式1
1 | function Person(name) { |
方式2
1 | function Person(name) { |
验证上面的 DEMO
1 | var person = new Person('张三'); // 正确 |
Demo2: 不能独立使用、必须继承后才能使用的类
1 | class Shape { |
17.9 Mixin模式的实现
说明:指的是将多个类的接口“混入”(mixin)另一个类
原理:类本身其实就是一个特殊的对象,可以通过 Object.defineProperty()
为类添加成员(ES6的类的成员只有构造器和方法)
定义 Mixin 功能的实现
1 | /** |
应用
1 | class DistributedEdit extends mix(Loggable, Serializable) { |